Molecule test Ansible

Mons, 2019-03-21

Fabrice Flore-Thebault

About the author

243761
  • User & contributor in Molecule / Ansible ecosystem.

  • Free Software Infrastructure Automation.

Culture, Automation, Measurement, Sharing.

Ansible User stories

  • Day 2 routines: system patches, audit, inventory.

  • Reproductible provisioning, from hypervisor to apps.

  • Automated backup & restore data.

  • Maintain environments on shared hosting platforms.

  • Deploy software.

  • Build CI pipelines.

  • Manage everything API: network, cloud, kubernetes.

Common problem

Credit: National Library of Sweden, shelfmark KoB 1 ab
  • Validate roles and playbooks before production

  • Instantiate temporary infrastructure

Solution

molecule logo

Disclaimer

Wiki Loves Jules Verne Gustav Meyrink Der Golem 1915 %28Gerd Kueveler%29
Ansible is NOT a programing language

Tooling lanscape

Molecule has many friends in the toolbox.

  • Ansible ecosystem

  • Platforms backends

  • Dependency backends

  • Verifiers

Ansible

Ansible is an IT automation tool. It can configure systems, deploy software, and orchestrate more advanced IT tasks such as continuous deployments or zero downtime rolling updates. Ansible’s main goals are simplicity and ease-of-use.
— https://docs.ansible.com/ansible

Molecule

Molecule is designed to aid in the development and testing of Ansible roles. […​] Molecule is opinionated in order to encourage an approach that results in consistently developed roles that are well-written, easily understood and maintained.
— https://molecule.readthedocs.io

Ansible-lint

  • Improve the roles quality.

  • Kill opinion wars.

Ansible Lint is a commandline tool for linting playbooks. Use it to detect behaviors and practices that could potentially be improved.
— https://docs.ansible.com/ansible-lint

Backends

  • (local) Virtualization

  • Cloud provider

  • Bake your own

(local) Virtualization

  • Docker

  • LXC

  • LXD

  • Vagrant

Cloud provider

  • Azure

  • EC2

  • GCE

  • Linode

  • Openstack

Slow! Keep it for specific cloud features, Windows.

Bake your own

  • Delegated

Verifier

Audit the state of the tested platform after role execution with an independant tool.

  • Testinfra

  • Goss

  • Inspec

Testinfra

  • Default verifier.

  • Write tests in python.

  • Public == python developers.

With Testinfra you can write unit tests in Python to test actual state of your servers configured by management tools.

— https://testinfra.readthedocs.io

Goss

  • Easy. YAML syntax, fit well in the Ansible ecosystem.

  • Fast. Near instantaneous.

  • Small. <10MB single self-contained binary.

  • Linux only.

Goss is a YAML based serverspec alternative tool for validating a server’s configuration.
— https://goss.rocks

Inspec

  • Complex, ruby based, with a feature full DSL.

  • Linux, MacOS and Windows support.

  • Public == ruby developers.

InSpec is compliance as code. Turn your compliance, security, and other policy requirements into automated tests.
— https://www.inspec.io/

Molecule scenario dissected

$ molecule matrix -s default test
--> Test matrix

└── default
    ├── lint
    ├── cleanup
    ├── destroy
    ├── dependency
    ├── syntax
    ├── create
    ├── prepare
    ├── converge
    ├── idempotence
    ├── side_effect
    ├── verify
    ├── cleanup
    └── destroy

lint

Enforce syntax rules (ansible-lint, yamllint).
Kill opinion wars and improve the roles quality.
--> Executing Ansible Lint on molecule/default/playbook.yml...
    [701] No 'galaxy_info' found
    meta/main.yml:1

    [306] Shells that use pipes should set the pipefail option
    molecule/default/playbook.yml:20
    Task/Handler: shell | get version of common_linux

    [206] Variables should have spaces before and after: {{ var_name }}
    tasks/task_60_cron.yml:19
        path: "/etc/cron.{{cron_item}}"

cleanup

To be used in conjunction with prepare
Cleanup changes that were made outside of Molecule’s test platforms.
remote database connections
user accounts
--> Action: 'cleanup'
Skipping, cleanup playbook not configured.

destroy

Destroy the temporary platforms.
molecule destroy
--> Scenario: 'default'
--> Action: 'destroy'

    PLAY [Destroy] *****************************************************************

    TASK [Destroy molecule instance(s)] ********************************************
    changed: [localhost] => (item={'name': 'kind-default', 'groups': ['k8s'], 'image': 'bsycorp/kind:latest-1.12', 'privileged': True, 'override_command': False, 'exposed_ports': ['8443/tcp', '10080/tcp'], 'published_ports': ['0.0.0.0:9443:8443/tcp'], 'pre_build_image': True})

    TASK [Wait for instance(s) deletion to complete] *******************************
    FAILED - RETRYING: Wait for instance(s) deletion to complete (300 retries left).
    changed: [localhost] => (item={'started': 1, 'finished': 0, 'ansible_job_id': '498157453262.13184', 'results_file': '/home/fab/.ansible_async/498157453262.13184', '_ansible_parsed': True, 'changed': True, '_ansible_no_log': False, 'failed': False, 'item': {'name': 'kind-default', 'groups': ['k8s'], 'image': 'bsycorp/kind:latest-1.12', 'privileged': True, 'override_command': False, 'exposed_ports': ['8443/tcp', '10080/tcp'], 'published_ports': ['0.0.0.0:9443:8443/tcp'], 'pre_build_image': True}, '_ansible_item_result': True, '_ansible_ignore_errors': None, '_ansible_item_label': {'name': 'kind-default', 'groups': ['k8s'], 'image': 'bsycorp/kind:latest-1.12', 'privileged': True, 'override_command': False, 'exposed_ports': ['8443/tcp', '10080/tcp'], 'published_ports': ['0.0.0.0:9443:8443/tcp'], 'pre_build_image': True}})

    TASK [Delete docker network(s)] ************************************************

    PLAY RECAP *********************************************************************
    localhost                  : ok=2    changed=2    unreachable=0    failed=0

dependency

Install external requirements with supported backends:
galaxy: default. Lots of WIP in the air.
gilt: nice overlays on top of git.
shell: your own.
--> Action: 'dependency'
    - downloading role 'repo-remi', owned by geerlingguy
    - downloading role from https://github.com/geerlingguy/ansible-role-repo-remi/archive/1.2.0.tar.gz
    - extracting geerlingguy.repo-remi to /tmp/molecule/ansible-role-phpmyadmin/default/roles/geerlingguy.repo-remi
    - geerlingguy.repo-remi (1.2.0) was installed successfully
    - downloading role 'apache', owned by geerlingguy
    - downloading role from https://github.com/geerlingguy/ansible-role-apache/archive/3.0.3.tar.gz
    - extracting geerlingguy.apache to /tmp/molecule/ansible-role-phpmyadmin/default/roles/geerlingguy.apache
    - geerlingguy.apache (3.0.3) was installed successfully
    - downloading role 'mysql', owned by geerlingguy
    - downloading role from https://github.com/geerlingguy/ansible-role-mysql/archive/2.9.4.tar.gz
    - extracting geerlingguy.mysql to /tmp/molecule/ansible-role-phpmyadmin/default/roles/geerlingguy.mysql
    - geerlingguy.mysql (2.9.4) was installed successfully
    - downloading role 'php-versions', owned by geerlingguy
    - downloading role from https://github.com/geerlingguy/ansible-role-php-versions/archive/3.0.0.tar.gz
    - extracting geerlingguy.php-versions to /tmp/molecule/ansible-role-phpmyadmin/default/roles/geerlingguy.php-versions
    - geerlingguy.php-versions (3.0.0) was installed successfully
    - downloading role 'php', owned by geerlingguy
    - downloading role from https://github.com/geerlingguy/ansible-role-php/archive/3.7.0.tar.gz
    - extracting geerlingguy.php to /tmp/molecule/ansible-role-phpmyadmin/default/roles/geerlingguy.php
    - geerlingguy.php (3.7.0) was installed successfully
    - downloading role 'php-mysql', owned by geerlingguy
    - downloading role from https://github.com/geerlingguy/ansible-role-php-mysql/archive/2.0.2.tar.gz
    - extracting geerlingguy.php-mysql to /tmp/molecule/ansible-role-phpmyadmin/default/roles/geerlingguy.php-mysql
    - geerlingguy.php-mysql (2.0.2) was installed successfully

syntax

ansible-playbook --syntax-check
Complementary to lint, but need the external roles to work.
molecule syntax
--> Scenario: 'default'
--> Action: 'syntax'

    playbook: molecule/default/playbook.yml

create

Create the temporary platforms.
Local backends are faster and more reliable.
Cloud backends: abuse of retry, be patient
--> Scenario: 'default'
--> Action: 'create'

    PLAY [Create] ******************************************************************

    TASK [Log into a Docker registry] **********************************************
    skipping: [localhost] => (item={'name': 'kind-default', 'groups': ['k8s'], 'image': 'bsycorp/kind:latest-1.12', 'privileged': True, 'override_command': False, 'exposed_ports': ['8443/tcp', '10080/tcp'], 'published_ports': ['0.0.0.0:9443:8443/tcp'], 'pre_build_image': True})

    TASK [Create Dockerfiles from image names] *************************************
    skipping: [localhost] => (item={'name': 'kind-default', 'groups': ['k8s'], 'image': 'bsycorp/kind:latest-1.12', 'privileged': True, 'override_command': False, 'exposed_ports': ['8443/tcp', '10080/tcp'], 'published_ports': ['0.0.0.0:9443:8443/tcp'], 'pre_build_image': True})

    TASK [Discover local Docker images] ********************************************
    ok: [localhost] => (item={'changed': False, 'skipped': True, 'skip_reason': 'Conditional result was False', '_ansible_no_log': False, 'item': {'name': 'kind-default', 'groups': ['k8s'], 'image': 'bsycorp/kind:latest-1.12', 'privileged': True, 'override_command': False, 'exposed_ports': ['8443/tcp', '10080/tcp'], 'published_ports': ['0.0.0.0:9443:8443/tcp'], 'pre_build_image': True}, '_ansible_item_result': True, '_ansible_ignore_errors': None, '_ansible_item_label': {'name': 'kind-default', 'groups': ['k8s'], 'image': 'bsycorp/kind:latest-1.12', 'privileged': True, 'override_command': False, 'exposed_ports': ['8443/tcp', '10080/tcp'], 'published_ports': ['0.0.0.0:9443:8443/tcp'], 'pre_build_image': True}})

    TASK [Build an Ansible compatible image] ***************************************
    skipping: [localhost] => (item={'changed': False, 'skipped': True, 'skip_reason': 'Conditional result was False', '_ansible_no_log': False, 'item': {'name': 'kind-default', 'groups': ['k8s'], 'image': 'bsycorp/kind:latest-1.12', 'privileged': True, 'override_command': False, 'exposed_ports': ['8443/tcp', '10080/tcp'], 'published_ports': ['0.0.0.0:9443:8443/tcp'], 'pre_build_image': True}, '_ansible_item_result': True, '_ansible_ignore_errors': None, '_ansible_item_label': {'name': 'kind-default', 'groups': ['k8s'], 'image': 'bsycorp/kind:latest-1.12', 'privileged': True, 'override_command': False, 'exposed_ports': ['8443/tcp', '10080/tcp'], 'published_ports': ['0.0.0.0:9443:8443/tcp'], 'pre_build_image': True}})

    TASK [Create docker network(s)] ************************************************

    TASK [Determine the CMD directives] ********************************************
    skipping: [localhost] => (item={'name': 'kind-default', 'groups': ['k8s'], 'image': 'bsycorp/kind:latest-1.12', 'privileged': True, 'override_command': False, 'exposed_ports': ['8443/tcp', '10080/tcp'], 'published_ports': ['0.0.0.0:9443:8443/tcp'], 'pre_build_image': True})

    TASK [Create molecule instance(s)] *********************************************
    changed: [localhost] => (item={'name': 'kind-default', 'groups': ['k8s'], 'image': 'bsycorp/kind:latest-1.12', 'privileged': True, 'override_command': False, 'exposed_ports': ['8443/tcp', '10080/tcp'], 'published_ports': ['0.0.0.0:9443:8443/tcp'], 'pre_build_image': True})

    TASK [Wait for instance(s) creation to complete] *******************************
    FAILED - RETRYING: Wait for instance(s) creation to complete (300 retries left).
    changed: [localhost] => (item={'started': 1, 'finished': 0, 'ansible_job_id': '57155955616.7787', 'results_file': '/home/fab/.ansible_async/57155955616.7787', '_ansible_parsed': True, 'changed': True, '_ansible_no_log': False, 'failed': False, 'item': {'name': 'kind-default', 'groups': ['k8s'], 'image': 'bsycorp/kind:latest-1.12', 'privileged': True, 'override_command': False, 'exposed_ports': ['8443/tcp', '10080/tcp'], 'published_ports': ['0.0.0.0:9443:8443/tcp'], 'pre_build_image': True}, '_ansible_item_result': True, '_ansible_ignore_errors': None, '_ansible_item_label': {'name': 'kind-default', 'groups': ['k8s'], 'image': 'bsycorp/kind:latest-1.12', 'privileged': True, 'override_command': False, 'exposed_ports': ['8443/tcp', '10080/tcp'], 'published_ports': ['0.0.0.0:9443:8443/tcp'], 'pre_build_image': True}})

    PLAY RECAP *********************************************************************
    localhost                  : ok=3    changed=2    unreachable=0    failed=0

prepare

Optional actions which bring the system to a given state prior to converge.
remote database connections
user accounts
kubeconfig
--> Scenario: 'default'
--> Action: 'prepare'

    PLAY [Prepare] *****************************************************************

    TASK [delete the kubeconfig if present] ****************************************
    ok: [kind-default -> localhost]

    TASK [Fetch the kubeconfig] ****************************************************
    changed: [kind-default]

    TASK [Change the kubeconfig port to the proper value] **************************
    changed: [kind-default -> localhost]

    TASK [Wait for the Kubernetes API to become available (this could take a minute)] ***
    FAILED - RETRYING: Wait for the Kubernetes API to become available (this could take a minute) (60 retries left).
    FAILED - RETRYING: Wait for the Kubernetes API to become available (this could take a minute) (59 retries left).
    FAILED - RETRYING: Wait for the Kubernetes API to become available (this could take a minute) (58 retries left).
    FAILED - RETRYING: Wait for the Kubernetes API to become available (this could take a minute) (57 retries left).
    FAILED - RETRYING: Wait for the Kubernetes API to become available (this could take a minute) (56 retries left).
    ok: [kind-default]

    PLAY RECAP *********************************************************************
    kind-default               : ok=4    changed=2    unreachable=0    failed=0

converge

Execute the main role.
--> Scenario: 'default'
--> Action: 'converge'

    PLAY [Converge] ****************************************************************

    TASK [Gathering Facts] *********************************************************
    ok: [localhost]

    PLAY [Verify] ******************************************************************

    TASK [Gathering Facts] *********************************************************
    ok: [localhost]

    TASK [Get all pods in osdk-test] ***********************************************
    ok: [localhost]

    TASK [Output pods] *************************************************************
    ok: [localhost] => {
        "pods": {
            "changed": false,
            "failed": false,
            "resources": []
        }
    }

    PLAY RECAP *********************************************************************
    localhost                  : ok=4    changed=0    unreachable=0    failed=0


--> Scenario: 'default'
--> Action: 'idempotence'
Idempotence completed successfully.

idempotence

The main role is executed again; the result should change nothing in order to achieve idempotence.
Key feature!
Idempotence is a goal difficult to achieve, particularly on Windows.
--> Action: 'idempotence'
Idempotence completed successfully.

side_effect

Optional actions which are not in the role, after converge.
--> Scenario: 'default'
--> Action: 'side_effect'
Skipping, side_effect playbook not configured.

verify

Execute an audit tool to verify that the final state is meeting expectations.
testinfra - default, python based.
Goss - Linux only. Easy and fast.
Inspec - ruby DSL, works well for Windows targets.
--> Scenario: 'default'
--> Action: 'verify'
--> Executing Testinfra tests found in /home/fab/src/themr0c/talk-jdl2019/examples/geerlingguy.phpmyadmin/molecule/default/tests/...
    ============================= test session starts ==============================
    platform linux2 -- Python 2.7.16, pytest-4.3.1, py-1.8.0, pluggy-0.9.0
    rootdir: /home/fab/src/themr0c/talk-jdl2019/examples/geerlingguy.phpmyadmin/molecule/default, inifile:
    plugins: testinfra-1.16.0
collected 1 item

    tests/test_default.py .                                                  [100%]

    ========================== 1 passed in 12.76 seconds ===========================
Verifier completed successfully.

destroy

Destroy the temporary platforms.
molecule destroy
--> Scenario: 'default'
--> Action: 'destroy'

    PLAY [Destroy] *****************************************************************

    TASK [Destroy molecule instance(s)] ********************************************
    changed: [localhost] => (item={'name': 'kind-default', 'groups': ['k8s'], 'image': 'bsycorp/kind:latest-1.12', 'privileged': True, 'override_command': False, 'exposed_ports': ['8443/tcp', '10080/tcp'], 'published_ports': ['0.0.0.0:9443:8443/tcp'], 'pre_build_image': True})

    TASK [Wait for instance(s) deletion to complete] *******************************
    FAILED - RETRYING: Wait for instance(s) deletion to complete (300 retries left).
    changed: [localhost] => (item={'started': 1, 'finished': 0, 'ansible_job_id': '498157453262.13184', 'results_file': '/home/fab/.ansible_async/498157453262.13184', '_ansible_parsed': True, 'changed': True, '_ansible_no_log': False, 'failed': False, 'item': {'name': 'kind-default', 'groups': ['k8s'], 'image': 'bsycorp/kind:latest-1.12', 'privileged': True, 'override_command': False, 'exposed_ports': ['8443/tcp', '10080/tcp'], 'published_ports': ['0.0.0.0:9443:8443/tcp'], 'pre_build_image': True}, '_ansible_item_result': True, '_ansible_ignore_errors': None, '_ansible_item_label': {'name': 'kind-default', 'groups': ['k8s'], 'image': 'bsycorp/kind:latest-1.12', 'privileged': True, 'override_command': False, 'exposed_ports': ['8443/tcp', '10080/tcp'], 'published_ports': ['0.0.0.0:9443:8443/tcp'], 'pre_build_image': True}})

    TASK [Delete docker network(s)] ************************************************

    PLAY RECAP *********************************************************************
    localhost                  : ok=2    changed=2    unreachable=0    failed=0

From molecule to delivery pipeline

  • Objective: validate that all roles are in a good shape, ready to deliver.

Scope definition

  • Test one role with molecule

  • Test multiple roles with tox

  • Automate on Continuous Integration platform

Test one role at user request, locally

  • Before committing any changes to Git (tradeoffs: blocking, slow, antivirus).

  • Execute all tests on the named role ${rolename}:

cd ${rolename}
molecule test --all

Tox

  • Orchestrate tests on a collection of roles.

  • Isolated python virtual environments.

tox aims to automate and standardize testing in Python. It is part of a larger vision of easing the packaging, testing and release process of Python software.
— https://tox.readthedocs.io
pip install tox virtualenv

Configure tox.ini

  • Molecule role == Tox named environment

  • Running platforms are not isolated!

[tox]
envlist =
  my_example_role
  another_role
skipsdist = true
[testenv]
basepython = python3
commands = bash -c "(cd {toxinidir}/roles/{envname} && molecule --debug
test --all)"
description = molecule test role {envname}
deps = -r {toxinidir}/requirements.txt
setenv = MOLECULE_EPHEMERAL_DIRECTORY={envname}
sitepackages = true
whitelist_externals =
  /bin/bash
  /usr/bin/rubocop

Execute tox tests

tox (1)
tox -e ${rolename} (2)
1Execute all molecule tests on all roles
2Same, limited to the named role ${rolename}

Automation on a continuous integration server

  • On pull request. Never merge broken code!

  • At commit on release branches / tags.

  • Foresee long compute time !

  • Need to run privileged Docker containers

Use molecule docker image

docker_image_name="quay.io/ansible/molecule:2.19" (1)
gid_map="$(grep docker /etc/group |cut -d: -f3,3)" (2)
local_playbooks_absolute_path="$(readlink -f "$(dirname "${BASH_SOURCE[0]}")")" (3)
docker run \
  -e "WORKDIR=${molecule_workdir}" \ (3)
  -v /var/run/docker.sock:/var/run/docker.sock \ (4)
  -v "${local_playbooks_absolute_path}:${molecule_workdir}" (3)
  "${docker_image_name}" bash "molecule test"
1Docker image from https://quay.io/repository/ansible/molecule
2Map local user
3Mount the code
4Share docker socket

Orchestration pitfalls

1 repository == 1 role CAUTION: multiple roles ⇒ external orchestration * scenarios ⇒ sequential * platforms ⇒ parallel

Implementation examples

  • Kubernetes Operator SDK

  • geerlingguy.phpmyadmin

  • Create our own

Kubernetes Operator SDK

Prerequisites

  • git

  • docker version 17.03+.

  • kubectl version v1.9.0+.

  • ansible version v2.6.0+

  • ansible-runner version v1.1.0+

  • ansible-runner-http version v1.0.0+

  • dep version v0.5.0+.

  • go version v1.10+.

  • Access to a Kubernetes v.1.9.0+ cluster.

Install Operator SDK

export GOPATH=$HOME/.go
mkdir -p $GOPATH/src/github.com/operator-framework
cd $GOPATH/src/github.com/operator-framework
git clone https://github.com/operator-framework/operator-sdk
cd operator-sdk
git checkout master
make dep
make install

Create new operator-sdk project

operator-sdk new memcached-operator --api-version=cache.example.com/v1alpha1 --kind=Memcached --type=ansible
cd memcached-operator

Prepare dedicated virtualenv

mkvirtualenv -p /usr/bin/python3 operator-sdk
pip install docker openshift git+https://github.com/ansible/molecule.git

Test the new project

molecule test

Phpmyadmin role from Galaxy

sudo aptitude install python3 python3-dev python3-psutil
mkvirtualenv -p /usr/bin/python3 molecule-python3
pip install molecule docker
git clone git@github.com:geerlingguy/ansible-role-phpmyadmin.git geerlingguy.phpmyadmin
cd geerlingguy.phpmyadmin
molecule test

Create a new role

  • Demo time

Molecule community update

ansible-lint and molecule are great tools. They’ve been built and tested by the community that we see as essential parts of enhancing development of Ansible automation. By adopting these tools, Red Hat intends to invest resources working with the community to make them even better.

2018-09-26
— @tima (Timothy Appnel)

Molecule working group

workinggroup

Continuous integration

ci

Pre release 2.20a2

release

Ansible Collections

We want molecule to become the defacto standard tool, or sdk if you will, for content creators.

2019-03-13
— @thaumos (Dylan Sylva)